"use strict";

var path = require('path'),
    URL = require('url'),
    QS = require('querystring'),
    S = require('string'),
    _ = require('underscore'),
    E = require('./error.js'),
    ArgumentError = E.ArgumentError,
    ConfigError = E.ConfigError;


/**
 * @module Misc
 * @summary Miscellaneous utitlies
 */
var Self = module.exports =
{

    /**
     * Require a js module from a string.
     * @method
     * @param {string} src - The string containing the module's js code
     * @param {string} filename - The fake filename of the module to be loaded
     * @param {module} parent - The parent module of the module to be loaded
     * @param {object} [inject] - Any global variables to be used within the module
     * @returns {*}
     */
    requireFromString: function (src, filename, parent, inject) {
        var Module = module.constructor;
        var m = new Module(filename, parent);
        m.filename = filename;
        m.paths = Module._nodeModulePaths(path.dirname(filename));

        if (inject) {
            m = _.extend(m, inject);
        }

        m._compile(src, filename);
        return m.exports;
    },

    //url related-----------

    /**
     * Merge the query parameters into given url.
     * @method
     * @param {string} url - Original url.
     * @param {object} query - Key-value pairs query object to be merged into the url.
     * @returns {string}
     */
    extendQuery: function (url, query) {
        if (!query) return url;

        if (url.indexOf('?') === -1) {
            if (_.isObject(query)) {
                return url + '?' + QS.stringify(query);
            }

            return url + '?' + query;
        }

        var urlObj = URL.parse(url, true);
        if (_.isObject(query)) {
            delete urlObj.search;
            urlObj.query = _.extend(urlObj.query, query);
        } else {
            urlObj.search += '&' + query;
        }

        return URL.format(urlObj);
    },

    /**
     * Join url parts by adding necessary '/'
     * @method
     * @param {string} base - Left part
     * @param {string} path - Right part
     * @returns {string}
     */
    urlJoin: function (base, path) {
        return Self.trimRightSlash(base) + Self.ensureLeftSlash(path);
    },

    //path related-----------

    /**
     * Trim left '/' of a path
     * @method
     * @param {string} path - The path
     * @returns {string}
     */
    trimLeftSlash: function (path) {
        return S(path).chompLeft('/').s;
    },

    /**
     * Trim right '/' of a path
     * @method
     * @param {string} path - The path
     * @returns {string}
     */
    trimRightSlash: function (path) {
        return S(path).chompRight('/').s;
    },

    /**
     * Add a '/' to the left of a path if it does not have one
     * @method
     * @param {string} path - The path
     * @returns {string}
     */
    ensureLeftSlash: function (path) {
        return S(path).ensureLeft('/').s;
    },

    /**
     * Add a '/' to the right of a path if it does not have one
     * @method
     * @param {string} path - The path
     * @returns {string}
     */
    ensureRightSlash: function (path) {
        return S(path).ensureRight('/').s;
    },

    //string related-----------

    /**
     * Indent a text block with spaces
     * @method
     * @param {string} block - The path
     * @param {number} [num=4] - Number of spaces
     * @returns {string}
     */
    indentBlock: function (block, num) {
        num = num || 4;

        var space = S(' ').repeat(num).s,
            rpl = '\n' + space;

        return space + S(block).replaceAll('\n', rpl).s;
    },

    /**
     * Escape a regular expression
     * @method
     * @param {string} s - The regular expression string
     * @returns {string}
     */
    escapeRegExpToken: function(s) {
        return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    },

    //collection related-----------

    getValueByPath: function (collection, path, defaultValue) {
        var nodes = path.split('.'),
            value = collection;

        if (!value) {
            return defaultValue;
        }

        if (nodes.length === 0) return null;

        _.find(nodes, function(e) {
            value = value[e];
            return typeof value === 'undefined';
        });

        return value || defaultValue;
    },

    /**
     * Project an array of object to a key-value table.
     * @method
     * @param {object[]} objectArray - The object array to be projected
     * @param {string} keyProperty - The name of the property to be used as key
     * @param {string} [valProperty] - The name of the property to be used as value, if not provided, use the whole object instead
     * @returns {object}
     */
    objectProjection: function (objectArray, keyProperty, valProperty) {
        var result = {};
        _.map(objectArray, function (obj) {
            var k = obj[keyProperty];
            result[k] = valProperty ? obj[valProperty] : obj;
        });

        return result;
    },

    /**
     * Copy all of the properties in the source objects over to a shallow clone of the destination object, and return the cloned object.
     * @method
     * @param {object} obj - The object to be cloned and extended
     * @param {object} source - An object with properties to be copied
     * @returns {object}
     */
    extendToCopy: function (obj, source) {
        return _.extend(_.clone(obj), source);
    },

    //validation related-----------
    throwRequired: function (name) {
        throw new ConfigError(name, 'Missing required config item.');
    },

    /**
     * Check the given string and throw an error if it contains a ".." substring.
     * @method
     * @param {object} name - The variable name
     * @param {object} str - The value
     * @throws {ArgumentError}
     */
    excludeDotDotInPath: function (name, str) {

        if (str.indexOf('..') >= 0) {
            throw new ArgumentError(name, '".." not allowed.');
        }
    },

    /**
     * Check the given string and throw an error if it contains a "." character.
     * @method
     * @param {object} name - The variable name
     * @param {object} str - The value
     * @throws {ArgumentError}
     */
    excludeDotChar: function (name, str) {

        if (str.indexOf('.') >= 0) {
            throw new ArgumentError(name, '"." not allowed.');
        }
    }
};


